
/*
 * Copyright (C) 2008-2009 Archie L. Cobbs. All rights reserved.
 *
 * $Id: ClassGenerator.java 317 2010-02-11 22:06:51Z archie.cobbs $
 */

package org.dellroad.sidekar.classgen;

import static org.dellroad.sidekar.asm.Opcodes.ACC_FINAL;
import static org.dellroad.sidekar.asm.Opcodes.ACC_PRIVATE;
import static org.dellroad.sidekar.asm.Opcodes.ACC_PUBLIC;
import static org.dellroad.sidekar.asm.Opcodes.ACC_SUPER;
import static org.dellroad.sidekar.asm.Opcodes.ACC_SYNTHETIC;
import static org.dellroad.sidekar.asm.Opcodes.ALOAD;
import static org.dellroad.sidekar.asm.Opcodes.CHECKCAST;
import static org.dellroad.sidekar.asm.Opcodes.INVOKESPECIAL;
import static org.dellroad.sidekar.asm.Opcodes.LLOAD;
import static org.dellroad.sidekar.asm.Opcodes.PUTFIELD;
import static org.dellroad.sidekar.asm.Opcodes.RETURN;
import static org.dellroad.sidekar.asm.Opcodes.V1_5;

import org.dellroad.sidekar.Entity;
import org.dellroad.sidekar.Item;
import org.dellroad.sidekar.asm.ClassWriter;
import org.dellroad.sidekar.asm.FieldVisitor;
import org.dellroad.sidekar.asm.MethodVisitor;
import org.dellroad.sidekar.asm.Type;
import org.dellroad.sidekar.schema.model.EntityModel;
import org.dellroad.sidekar.schema.model.FieldModel;

/**
 * Generates the classes that serve as Java runtime representations of each Sidekar {@link Entity}.
 * Instances of the generated classes (i.e., the objects) are the Java runtime representations of
 * individual database items.
 *
 * <p>
 * The generated classes are subclasses of the the user model classes associated with the entity.
 * They implement the {@link Item} interface, and have their Sidekar field getter and setter
 * methods overridden.
 *
 * @see Item
 */
public class ClassGenerator extends MethodGeneratorSupport {

    /**
     * String to use for the "source file" attribute in generated classes.
     */
    public static final String GEN_SOURCE = "[SidekarGenerated]";

    private final EntityModel entityModel;
    private final String superName;

    /**
     * Constructor.
     *
     * @param entityModel schema entity model
     * @param className name of the generated class
     */
    public ClassGenerator(EntityModel entityModel, String className) {
        super(className);
        this.entityModel = entityModel;
        this.superName = Type.getInternalName(this.entityModel.getEntityClass());
    }

    /**
     * Generate the Java class bytecode for the entity given in the construtor.
     */
    public byte[] generateClass() {
        ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
        cw.visit(V1_5, ACC_PUBLIC | ACC_SUPER | ACC_FINAL | ACC_SYNTHETIC, this.className, null,
          this.superName, new String[] { Type.getInternalName(Item.class) });
        cw.visitSource(GEN_SOURCE, null);
        outputFields(cw);
        outputConstructor(cw);
        outputMethods(cw);
        cw.visitEnd();
        return cw.toByteArray();
    }

    /**
     * Generate a Java name for a field.
     */
    static String getGenFieldName(FieldModel fieldModel) {
        if (fieldModel.getName() == null)
            throw new IllegalArgumentException("internal error: field has no name: " + fieldModel);
        return "$field_" + fieldModel.getName();
    }

    /**
     * Get the ASM type associated with a field.
     */
    static Type getGenFieldType(FieldModel fieldModel) {
        return Type.getType(fieldModel.visit(new FieldTypeSwitch()));
    }

    private void outputFields(ClassWriter cw) {

        // Do "id" and "entity" fields
        FieldVisitor fv = cw.visitField(ACC_PRIVATE | ACC_FINAL, ID_FIELD, Type.LONG_TYPE.getDescriptor(), null, null);
        fv.visitEnd();
        fv = cw.visitField(ACC_PRIVATE | ACC_FINAL, ENTITY_FIELD, Type.getDescriptor(Entity.class), null, null);
        fv.visitEnd();

        // Do entity Field fields
        for (EntityModel ancestorModel = this.entityModel; ancestorModel != null; ancestorModel = ancestorModel.getParentModel()) {
            for (FieldModel fieldModel : ancestorModel.getFieldModels().values()) {
                fv = cw.visitField(ACC_PRIVATE | ACC_FINAL, getGenFieldName(fieldModel),
                  getGenFieldType(fieldModel).getDescriptor(), null, null);
                fv.visitEnd();
            }
        }
    }

    private void outputConstructor(ClassWriter cw) {

        // Do intial stuff
        MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, "<init>",
          Type.getMethodDescriptor(Type.VOID_TYPE, new Type[] { Type.LONG_TYPE, Type.getType(Entity.class) }), null, null);
        mv.visitCode();

        // Invoke superclass constructor
        mv.visitVarInsn(ALOAD, 0);
        mv.visitMethodInsn(INVOKESPECIAL, this.superName, "<init>", "()V");

        // Store "id" parameter into "id" field
        mv.visitVarInsn(ALOAD, 0);
        mv.visitVarInsn(LLOAD, 1);
        mv.visitFieldInsn(PUTFIELD, this.className, ID_FIELD, Type.LONG_TYPE.getDescriptor());

        // Store "entity" parameter into "entity" field
        mv.visitVarInsn(ALOAD, 0);
        mv.visitVarInsn(ALOAD, 3);
        mv.visitFieldInsn(PUTFIELD, this.className, ENTITY_FIELD, Type.getDescriptor(Entity.class));

        // Initialize fields with Field instances acquired from entity
        for (EntityModel ancestorModel = this.entityModel; ancestorModel != null; ancestorModel = ancestorModel.getParentModel()) {
            for (FieldModel fieldModel : ancestorModel.getFieldModels().values()) {
                mv.visitVarInsn(ALOAD, 0);
                mv.visitVarInsn(ALOAD, 3);
                mv.visitLdcInsn(fieldModel.getName());
                emitInvoke(mv, this.getFieldMethod);
                mv.visitTypeInsn(CHECKCAST, getGenFieldType(fieldModel).getInternalName());
                mv.visitFieldInsn(PUTFIELD, this.className,
                  getGenFieldName(fieldModel), getGenFieldType(fieldModel).getDescriptor());
            }
        }

        // Do final stuff
        mv.visitInsn(RETURN);
        mv.visitMaxs(0, 0);
        mv.visitEnd();
    }

    private void outputMethods(ClassWriter cw) {

        // Create snippet for pushing item's entity and item onto the stack
        CodeEmitter pushEntity = new CodeEmitter() {
            public void write(MethodVisitor mv) {
                getEntity(mv);
                mv.visitVarInsn(ALOAD, 0);
            }
        };

        // Output getSidekarId() method
        emitAccessor(cw, this.getSidekarIdMethod, ID_FIELD);

        // Output getSidekarEntity() method
        emitAccessor(cw, this.getSidekarEntityMethod, ENTITY_FIELD);

        // Output sidekarAttach() method
        forwardMethod(cw, this.sidekarAttachMethod, this.attachMethod, pushEntity, null);

        // Output sidekarDetach() method
        forwardMethod(cw, this.sidekarDetachMethod, this.detachMethod, pushEntity, null);

        // Output isSidekarAttached() method
        forwardMethod(cw, this.isSidekarAttachedMethod, this.isAttachedMethod, pushEntity, null);

        // Output sidekarValidate() method
        forwardMethod(cw, this.sidekarValidateMethod, this.validateMethod, pushEntity, null);

        // Output methods associated with each field
        for (EntityModel ancestorModel = this.entityModel; ancestorModel != null; ancestorModel = ancestorModel.getParentModel()) {
            for (FieldModel fieldModel : ancestorModel.getFieldModels().values())
                fieldModel.visit(new MethodGeneratorSwitch(this.className, cw));
        }
    }
}

